local ScaleFade = VisualThemes:New("Theme", "ScaleFade")

local L = VisualThemes:GetLocaleTable()


local fading, scaling, timeToTransition, minScale --DB locals

local framesHiding, framesShowing = {}, {} --current hiding and showing frames

local UpdateFrame = CreateFrame"Frame" --the update frame draws the transitions

local HideFrame, ShowFrame --functions that call the original functions
local IsFrameVisible, IsFrameShown


local specialFramesTheme, specialFramesDetheme --tables that hold information on special frames that need modified techniques to work

local Hide, Show, IsVisible, IsShown --functions that will replace the original methods

local SaveFrame, ResetFrame, UpdateFrameShow, UpdateFrameHide, IsFrameSaved --functions that control the transition states of a frame
do
	local scale, alpha = {}, {}
	SaveFrame = function(frame)
		scale[frame] = scaling and frame:GetScale()
		alpha[frame] = fading and frame:GetAlpha()
	end
	ResetFrame = function(frame)
		if scale[frame] then
			frame:SetScale(scale[frame])
		end
		if alpha[frame] then
			frame:SetAlpha(alpha[frame])
		end
	end
	IsFrameSaved = function(frame)
		return scale[frame] ~= nil and alpha[frame] ~= nil
	end
	UpdateFrameShow = function(frame, timeLeft)
		local percentComplete = (timeToTransition - timeLeft) / timeToTransition
		local intensity = minScale + percentComplete * (1 - minScale)

		if scale[frame] then
			frame:SetScale(scale[frame] * intensity)
		end
		if alpha[frame] then
			frame:SetAlpha(alpha[frame] * percentComplete)
		end

	end
	UpdateFrameHide = function(frame, timeLeft)
		local percentComplete = 1 - ((timeToTransition - timeLeft) / timeToTransition)
		local intensity = minScale + percentComplete * (1 - minScale)

		if scale[frame] then
			frame:SetScale(scale[frame] * intensity)
		end
		if alpha[frame] then
			frame:SetAlpha(alpha[frame] * percentComplete)
		end	
	end
end


do

	local RealHide, RealShow = UpdateFrame.Hide, UpdateFrame.Show
	local RealIsVisible, RealIsShown = UpdateFrame.IsVisible, UpdateFrame.IsShown
	
	local fastShow, fastHide = {}, {}
	local fastIsVisible, fastIsShown = {}, {}
	HideFrame = function(frame)
		if fastHide[frame] then
			RealHide(frame)
		elseif ScaleFade.hooks[frame] and ScaleFade.hooks[frame].Hide then
			ScaleFade.hooks[frame].Hide(frame)
		else
			frame:Hide()
		end
	end
	ShowFrame = function(frame)
		if fastShow[frame] then
			RealShow(frame)
		else
			ScaleFade.hooks[frame].Show(frame)
		end
	end
	IsFrameVisible = function(frame)
		if fastIsVisible[frame] then
			return RealIsVisible(frame)
		elseif ScaleFade:IsHooked(frame, "IsVisible") then
			return ScaleFade.hooks[frame].IsVisible(frame)
		else
			return frame:IsVisible()
		end
	end
	IsFrameShown = function(frame)
		if fastIsShown[frame] then
			return RealIsShown(frame)
		elseif ScaleFade:IsHooked(frame, "IsShown") then
			return ScaleFade.hooks[frame].IsShown(frame)
		else
			return frame:IsShown()
		end
	end

	--helper functions that determine which hook to call
	--Since frames all inherit from one metatable any Hide or Show function can be used as long as the frame is passed
	--Saving some memory over AceHook
	local function AddFastHook(frame, method, func)
		if method == "Show" then
			frame.Show = func
			fastShow[frame] = true
		elseif method == "Hide" then
			frame.Hide = func
			fastHide[frame] = true
		elseif method == "IsVisible" then
			frame.IsVisible = func
			fastIsVisible[frame] = true
		elseif method == "IsShown" then
			frame.IsShown = func
			fastIsShown[frame] = true
		end
	end
	local function RemoveFastHooks()
		for frame in pairs(fastShow) do
			frame.Show = RealShow
			fastShow[frame] = nil
		end
		for frame in pairs(fastHide) do
			frame.Hide = RealHide
			fastHide[frame] = nil
		end
		for frame in pairs(fastIsVisible) do
			frame.IsVisible = RealIsVisible
			fastIsVisible[frame] = nil
		end
		for frame in pairs(fastIsShown) do
			frame.IsShown = RealIsShown
			fastIsShown[frame] = nil
		end
	end
	local function RemoveFastHook(frame, method)
		if method == "Show" then
			if fastShow[frame] then
				frame.Show = RealShow
				fastShow[frame] = nil
			end
		elseif method == "Hide" then
			if fastHide[frame] then
				frame.Hide = RealHide
				fastHide[frame] = nil
			end
		elseif method == "IsVisible" then
			if fastIsVisible[frame] then
				frame.IsVisible = RealIsVisible
				fastIsVisible[frame] = nil
			end
		elseif method == "IsShown" then
			if fastIsShown[frame] then
				frame.IsShown = RealIsShown
				fastIsShown[frame] = nil
			end
		end
	end
	function ScaleFade:SmartHook(frame, method, func)
		if method == "Hide" then
			if frame.Hide == RealHide then
				AddFastHook(frame, method, func)
			else  --something has hooked it already
				self:Hook(frame, method, func, true)
			end
		elseif method == "Show" then
			if frame.Show == RealShow then
				AddFastHook(frame, method, func)
			else --we should honor the hook instead of destroying it
				self:Hook(frame, method, func, true)
			end
		elseif method == "IsVisible" then
			if frame.IsVisible == RealIsVisible then
				AddFastHook(frame, method, func)
			else
				self:Hook(frame, method, func, true)
			end
		elseif method == "IsShown" then
			if frame.IsShown == RealIsShown then
				AddFastHook(frame, method, func)
			else
				self:Hook(frame, method, func, true)
			end
		end
	end
	function ScaleFade:SmartUnhookAll()
		RemoveFastHooks()
		self:UnhookAll()
	end

	function ScaleFade:SmartUnhook(frame)
		local name = frame:GetName() or ""
		if specialFramesDetheme[name] then
			specialFramesDetheme[name](self, frame)
		end
		if self:IsHooked(frame, "Show") then
			self:Unhook(frame, "Show")
		else
			RemoveFastHook(frame, "Show")
		end
		if self:IsHooked(frame, "Hide") then
			self:Unhook(frame, "Hide")
		else
			RemoveFastHook(frame, "Hide")
		end
		if self:IsHooked(frame, "IsShown") then
			self:Unhook(frame, "IsShown")
		else
			RemoveFastHook(frame, "IsShown")
		end
		if self:IsHooked(frame, "IsVisible") then
			self:Unhook(frame, "IsVisible")
		else
			RemoveFastHook(frame, "IsVisible")
		end
	end

	Hide = function(frame)
		if IsFrameShown(frame) then
			if not framesHiding[frame] then
				local ttt = timeToTransition
				if framesShowing[frame] then
					ttt = ttt - framesShowing[frame]
					framesShowing[frame] = nil
				end
				framesHiding[frame] = ttt
				UpdateFrame:Show()
			end
		else
			HideFrame(frame) -- even though we know better, calling it is proper hooking
		end
	end

	Show = function(frame)
		if framesHiding[frame] or not IsFrameShown(frame) then
			ShowFrame(frame)
			

			local ttt = timeToTransition
			if framesHiding[frame] then
				ttt = ttt - framesHiding[frame]
				framesHiding[frame] = nil
			else
				SaveFrame(frame)
			end
			
			framesShowing[frame] = ttt
			UpdateFrame:Show()
		else
			ShowFrame(frame)-- even though we know better, calling it is proper hooking
		end
	end
	IsVisible = function(frame)
		if framesHiding[frame] then
			return
		end
		return IsFrameVisible(frame)
	end

	IsShown = function(frame)
		if framesHiding[frame] then
			return
		end
		return IsFrameShown(frame)
	end
end



function ScaleFade:Theme(frame)
	local name = frame:GetName() or ""
	if specialFramesTheme[name] then
		return specialFramesTheme[name](self, frame)
	end
	self:SmartHook(frame, "Show", Show)
	self:SmartHook(frame, "Hide", Hide)
	SaveFrame(frame)
end

function ScaleFade:Detheme(frame)
	self:SmartUnhook(frame)
end


function ScaleFade:OnDisable()
	self:SmartUnhookAll()
end

function ScaleFade:OnEnable(first)
	if not first then
		return
	end
	self.db = VisualThemes:AcquireDBNamespace(self.Name)
	VisualThemes:RegisterDefaults(self.Name, "profile", {Scaling = true, Fading = true, TimeToTransition = 0.15, MinScale = .8,})
	self.options = {
		type = "group",
		name = self.Name,
		desc = L["%s Options"]:format(self.Name),
		args = {
			Scaling = {
				type = "toggle",
				name = L["Scaling"],
				desc = L["Determines whether or not the theme should scale the frames on transitions."],
				get = function() return self.db.profile.Scaling end,
				set = function() self.db.profile.Scaling = not self.db.profile.Scaling scaling = self.db.profile.Scaling end,
				order = 1,
			},
			Fading = {
				type = "toggle",
				name = L["Fading"],
				desc = L["Determines whether or not the theme should fade the frames on transitions."],
				get = function() return self.db.profile.Fading end,
				set = function() self.db.profile.Fading = not self.db.profile.Fading fading = self.db.profile.Fading end,
				order = 2,
			},
			TimeToTransition = {
				type = "range",
				name = L["Time to Transition"],
				desc = L["Sets the amount of time, in seconds, to transition a frame."],
				get = function() return self.db.profile.TimeToTransition end,
				set = function(v) self.db.profile.TimeToTransition, timeToTransition = v, v end,
				max = .3,
				min = .1,
				step = .01,
				bigStep = .05,
				order = 3,
			},
			MinScale = {
				type = "range",
				name = L["Minimum Scale"],
				desc = L["The minimum (relative) amount a frame will scale to."],
				get = function() return self.db.profile.MinScale end,
				set = function(v) self.db.profile.MinScale, minScale = v, v end,
				isPercent = true,
				step = .01,
				bigStep = .1,
				order = 4,
			},
		},
	}
	fading, scaling, timeToTransition, minScale = self.db.profile.Fading, self.db.profile.Scaling, self.db.profile.TimeToTransition, self.db.profile.MinScale
end

do
	local framesToHide = {} -- temp table used to avoid modifying the framesHiding table while looping
	local pairs = pairs
	UpdateFrame:SetScript("OnUpdate", function(self, delta)
		local activeFrames = 0
		for frame in pairs(framesHiding) do
			if not IsFrameSaved(frame) then
				framesHiding[frame] = nil
				framesToHide[frame] = true
			else
				framesHiding[frame] = framesHiding[frame] - delta
				
				if framesHiding[frame] < 0 then
					framesHiding[frame] = nil
					ResetFrame(frame)
					framesToHide[frame] = true
				else
					UpdateFrameHide(frame, framesHiding[frame])
				end
			end
			activeFrames = activeFrames + 1

		end
		for frame in pairs(framesShowing) do
			if not IsFrameSaved(frame) then
				framesShowing[frame] = nil
			else
				framesShowing[frame] = framesShowing[frame] - delta

				if framesShowing[frame] < 0 then
					framesShowing[frame] = nil
					ResetFrame(frame)
				else
					UpdateFrameShow(frame, framesShowing[frame])
				end
			end
			activeFrames = activeFrames + 1
		end
		for frame in pairs(framesToHide) do
			HideFrame(frame)
			framesToHide[frame] = nil
		end
		--i realize that active frames are counted even if they're now hidden
		--this is done to prevent small timing errors, one extra update isn't gonna hurt anything
		if activeFrames == 0 then
			self:Hide()
		end
	end)
end


function ScaleFade:IsCompatible(frameName)
	if frameName == "GameTooltip" then
		return false, L["doesn't properly theme"]
	elseif frameName == "CraftFrame" then
		return not (ATSWFrame ~= nil or SkilletFrame ~= nil), L["isn't compatible when other tradeskill addons are loaded"]
	elseif frameName == "TradeSkillFrame" then
		if ATSWFrame ~= nil or SkilletFrame ~= nil or Hemlock ~= nil then
			return false, L["isn't compatible when other tradeskill addons are loaded"]
		end
	end
	return true
end

local function RemoveDewdrop(self, frame)
	self:Unhook(frame, "SetPoint")
	self:Unhook(frame, "ClearAllPoints")
end

specialFramesDetheme = {
	ChatFrameEditBox = function(self, frame)
		self:Unhook(frame, "OnTextChanged")
		self:Unhook(frame, "OnHide")
	end,
	Dewdrop20Level1 = RemoveDewdrop,
	Dewdrop20Level2 = RemoveDewdrop,
	Dewdrop20Level3 = RemoveDewdrop,
	Dewdrop20Level4 = RemoveDewdrop,
	Dewdrop20Level5 = RemoveDewdrop,
	Dewdrop20Level6 = RemoveDewdrop,
	Dewdrop20Level7 = RemoveDewdrop,
	Dewdrop20Level8 = RemoveDewdrop,
	Dewdrop20Level9 = RemoveDewdrop,
	Dewdrop20Level10 = RemoveDewdrop,
}

--hooks the IsVisible and IsShown methods to lie to the caller when the frame is in transition
--shouldn't be needed except in rares cases
local function VisibleShownHook(self, frame)
	self:SmartHook(frame, "Show", Show)
	self:SmartHook(frame, "Hide", Hide)
	self:SmartHook(frame, "IsVisible", IsVisible)
	self:SmartHook(frame, "IsShown", IsShown)
end

local function HandleDewdrop(self, frame)
	local ignoreOnce = false
	self:Hook(frame, "SetPoint", function(frame, ...)
		if not ignoreOnce then
			self.hooks[frame].SetPoint(frame, ...)
		else
			ignoreOnce = false
			local x, y = frame:GetCenter()
			self.hooks[frame].ClearAllPoints(frame)
			self.hooks[frame].SetPoint(frame, "CENTER", UIParent, "BOTTOMLEFT", x, y)
		end
	end, true)

	self:Hook(frame, "ClearAllPoints", function(frame, ...)
		if not ignoreOnce then
			self.hooks[frame].ClearAllPoints(frame, ...)
		end
	end, true)


	self:SmartHook(frame, "Show", Show)
	self:SmartHook(frame, "Hide", function(frame)
		if frame:IsShown() then
			ignoreOnce = true
		end
		Hide(frame)
	end)
end

local function ThemeDropDowns(self, frame)
	self:SmartHook(frame, "Show", Show)
	self:SecureHook(frame, "Hide", ResetFrame)
end

specialFramesTheme = {
	ChatFrameEditBox = function(self, frame)
		local lastChatCommand = ""
		self:HookScript(frame, "OnTextChanged", function(...) 
			lastChatCommand = frame:GetText()
			self.hooks[frame].OnTextChanged(...)
		end)
		local autofocus = frame:IsAutoFocus()
		self:SmartHook(frame, "Hide", function(frame)
			if framesHiding[frame] then
				if lastChatCommand:sub(1, 1) ~= "/" then
					HideFrame(frame)
					frame:Show()
				end
				return
			end
			Hide(frame)
			autofocus = frame:IsAutoFocus()
			frame:SetAutoFocus(false)
			frame:ClearFocus()

		end)
		self:SmartHook(frame, "Show", Show)

		self:HookScript(frame, "OnHide", function(...)
			self.hooks[frame].OnHide(...)
			frame:SetAutoFocus(autofocus)
		end)
	end,
	QuestFrame = VisibleShownHook,
	OpenMailFrame = function(self, frame)
		self:SmartHook(frame, "Show", Show)
		self:SmartHook(frame, "Hide", function(self)
			Hide(self)
			local onHide = self:GetScript("OnHide")
			if onHide then
				onHide(self)	
			end
		end)
	end,
 
	DropDownList1 = ThemeDropDowns,
	DropDownList2 = ThemeDropDowns,
	DropDownList3 = ThemeDropDowns,
	DropDownList4 = ThemeDropDowns,
	DropDownList5 = ThemeDropDowns,
	DropDownList6 = ThemeDropDowns,
	DropDownList7 = ThemeDropDowns,
	DropDownList8 = ThemeDropDowns,
	DropDownList9 = ThemeDropDowns,
	DropDownList10 = ThemeDropDowns,

	Dewdrop20Level1 = HandleDewdrop,
	Dewdrop20Level2 = HandleDewdrop,
	Dewdrop20Level3 = HandleDewdrop,
	Dewdrop20Level4 = HandleDewdrop,
	Dewdrop20Level5 = HandleDewdrop,
	Dewdrop20Level6 = HandleDewdrop,
	Dewdrop20Level7 = HandleDewdrop,
	Dewdrop20Level8 = HandleDewdrop,
	Dewdrop20Level9 = HandleDewdrop,
	Dewdrop20Level10 = HandleDewdrop,

	TInvFrame = VisibleShownHook,
	OneBagFrame = VisibleShownHook,
}


for i=1, STATICPOPUP_NUMDIALOGS do
	specialFramesTheme["StaticPopup"..i] = VisibleShownHook
end

for i=1, NUM_GROUP_LOOT_FRAMES do
	specialFramesTheme["GroupLootFrame"..i] = VisibleShownHook
end